spb/crypto-trade-bot
Public
Python 54.7%
TypeScript 42.5%
SQL 1.9%
1"use client";2import { useQuery } from "@tanstack/react-query";3import { Download, FileText } from "lucide-react";4import Link from "next/link";5import { useParams } from "next/navigation";6import { useState } from "react";7import { StatusBadge } from "../../page";8import { LinesChart } from "@/components/charts";9import { BenchmarkTable, EquityVsBenchmarks, MetricsGrid, Provenance, RejectedOrders, RollingSharpe, SensitivityHeatmap, ShuffledTrades, StressTable, TradeLedger, Underwater, WalkForward } from "@/components/run-views";10import { Badge, Card, Empty, ErrorBox, PageHeader, Spinner } from "@/components/ui";11import { api } from "@/lib/api";12import { fmtTs } from "@/lib/format";13import type { Run, StrategyMeta } from "@/lib/types";1415export default function RunPage() {16 const { id } = useParams<{ id: string }>();17 const run = useQuery({ queryKey: ["run", id], queryFn: () => api<Run>(`/api/backtests/${id}`), refetchInterval: (q) => (q.state.data && (q.state.data.status === "done" || q.state.data.status === "failed") ? false : 1_500) });18 const strategies = useQuery({ queryKey: ["strategies"], queryFn: () => api<{ items: StrategyMeta[] }>("/api/strategies") });19 const [page, setPage] = useState(0);20 if (run.isLoading) return <Spinner label="Loading run…" />;21 if (run.error) return <ErrorBox error={run.error} />;22 const r = run.data!;23 const meta = strategies.data?.items.find((s) => s.name === r.strategy);24 const a = r.artifacts;25 const m = r.metrics;26 return (27 <>28 <PageHeader29 title={`${meta?.label ?? r.strategy} · ${r.symbol} · ${r.interval_min}m`}30 subtitle={<span className="flex flex-wrap items-center gap-2"><StatusBadge status={r.status} />{meta?.experimental && <Badge tone="warn">experimental — not validated for live use</Badge>}{r.label && <span className="text-fg-muted">{r.label}</span>}{r.start_ts && <span className="num text-fg-dim">{fmtTs(r.start_ts)} → {fmtTs(r.end_ts)}</span>}</span>}31 actions={<>32 <Link href={`/reports/${r.id}`} className="inline-flex h-8 items-center gap-1 rounded-md border border-line-strong px-2.5 text-xs hover:bg-bg-sub"><FileText className="h-3.5 w-3.5" /> Report</Link>33 <a href={`/api/backtests/${r.id}`} className="inline-flex h-8 items-center gap-1 rounded-md border border-line-strong px-2.5 text-xs hover:bg-bg-sub" download={`backtest-${r.id}.json`}><Download className="h-3.5 w-3.5" /> JSON</a>34 <a href={`/api/backtests/${r.id}/trades.csv`} className="inline-flex h-8 items-center gap-1 rounded-md border border-line-strong px-2.5 text-xs hover:bg-bg-sub" download><Download className="h-3.5 w-3.5" /> Trades CSV</a>35 </>}36 />37 {r.status === "failed" && <ErrorBox error={r.error ?? "run failed"} />}38 {(r.status === "queued" || r.status === "running") && <Spinner label={`Backtest ${r.status}… (benchmarks, bootstrap and stress scenarios run in the same job)`} />}39 {r.status === "done" && a && m && (40 <div className="flex flex-col gap-4">41 <Card title="Performance (in-sample window)" subtitle="Net of fees, spread, slippage and impact. Sharpe shows a 95% block-bootstrap interval; the deflated Sharpe is the probability the Sharpe exceeds what the best of N random trials would produce."><MetricsGrid m={m} /></Card>42 <div className="grid gap-4 xl:grid-cols-3">43 <Card title="Equity vs benchmarks" className="xl:col-span-2"><EquityVsBenchmarks a={a} label={r.strategy} /></Card>44 <Card title="Benchmarks" subtitle="Same window, same cost model."><BenchmarkTable a={a} m={m} /></Card>45 </div>46 <div className="grid gap-4 md:grid-cols-2">47 <Card title="Underwater (drawdown)"><Underwater a={a} /></Card>48 <Card title="Rolling Sharpe"><RollingSharpe a={a} /></Card>49 </div>50 {a.holdout ? (51 <Card title="Out-of-sample holdout" subtitle={`Final ${Math.round(Number(r.provenance?.holdout_frac ?? 0) * 100)}% of the data, never used for selection. Starts ${fmtTs(a.holdout.start_ts)}.`}>52 <MetricsGrid m={a.holdout.metrics} compact />53 <div className="mt-3"><LinesChart series={[{ id: "h", label: "holdout equity", color: "#22c55e", points: a.holdout.equity.map((p) => ({ ts: p.ts, value: p.equity })) }]} height={200} /></div>54 </Card>55 ) : <Card title="Out-of-sample holdout"><Empty title="No holdout window">Set a holdout fraction > 0 in the Strategy Lab to reserve a final test slice.</Empty></Card>}56 <Card title="Walk-forward folds"><WalkForward wf={a.walk_forward} /></Card>57 <div className="grid gap-4 md:grid-cols-2">58 <Card title="Parameter sensitivity"><SensitivityHeatmap s={a.sensitivity} /></Card>59 <Card title="Stress scenarios" subtitle="Re-runs with adverse cost assumptions and a simulated exchange outage gap."><StressTable stress={a.stress} base={m} /></Card>60 </div>61 <div className="grid gap-4 md:grid-cols-2">62 <Card title="Shuffled-trades Monte-Carlo" subtitle="Drawdown distribution when round-trip P&Ls are reordered."><ShuffledTrades s={a.shuffled_trades} /></Card>63 <Card title="Rejected orders" subtitle="Blocked by the risk gateway during simulation."><RejectedOrders rejected={a.rejected} /></Card>64 </div>65 <Card title={`Trade ledger (${a.trades.length}${a.trades.length >= 2000 ? ", last 2000 shown" : ""})`}><TradeLedger trades={a.trades} page={page} setPage={setPage} /></Card>66 <Card title="Provenance" subtitle="Everything needed to reproduce this run."><Provenance run={r} /></Card>67 {meta && (68 <Card title="Strategy notes">69 <p className="text-xs text-fg-muted">{meta.hypothesis}</p>70 {meta.failure_modes.length > 0 && <p className="mt-2 text-xs"><span className="text-fg-dim">Known failure modes:</span> {meta.failure_modes.join(" · ")}</p>}71 {meta.references.length > 0 && <p className="mt-1 text-xs"><span className="text-fg-dim">References:</span> {meta.references.join("; ")}</p>}72 </Card>73 )}74 </div>75 )}76 </>77 );78}79